use std::path::PathBuf;

use image::{ImageBuffer, RgbaImage};
use rstest::rstest;

use super::super::*;
use crate::tests::common::assert_expected_error;
use crate::tests::fixtures::{
    rand_float,
    rand_string,
    rand_usize,
    random_connectors,
    random_text_tile_content_vector,
    test_data,
    RANDOM_STRINGS,
};

#[test]
fn test_tile_raw_initialization() {
    let id = rand_usize(0, 234);
    let name = rand_string(rand_usize(0, 20));
    let content = random_text_tile_content_vector();
    let connectors = random_connectors();
    let weight = rand_float();

    let tile = Tile {
        id,
        name: name.clone(),
        content: TileContent::Text(content.clone()),
        connectors: Connectors { ..connectors },
        weight,
    };

    assert_eq!(tile.id, id);
    assert_eq!(tile.name, name);
    if let TileContent::Text(actual_content) = tile.content {
        assert_eq!(actual_content, content);
    } else {
        panic!("Expected the Text variant, got something else")
    }
    assert_eq!(tile.connectors, connectors);
    assert_eq!(tile.weight, weight);
}

#[test]
fn test_tile_has_zero_type_parameters() {
    // This line will block compilation if Tile takes type parameters,
    // thereby representing a kind of test that fails if this condition is not met.
    type _T = Tile;
}

#[test]
fn test_tile_empty_raw_initialization() {
    let id = rand_usize(0, 43);
    let name = rand_string(rand_usize(0, 20));
    let connectors = random_connectors();
    let weight = rand_float();

    let tile = Tile {
        id,
        name: name.clone(),
        content: TileContent::Empty,
        connectors: Connectors { ..connectors },
        weight,
    };

    assert_eq!(tile.id, id);
    assert_eq!(tile.name, name);
    if let TileContent::Empty = tile.content {
    } else {
        panic!("Expected the Empty variant, got something else")
    }
    assert_eq!(tile.connectors, connectors);
    assert_eq!(tile.weight, weight);
}

#[rstest]
fn test_tile_text_raw_initialization(
    #[values(
        vec![],
        vec!["".to_string()],
        vec![rand_string(1)],
        vec![rand_string(7)],
        random_text_tile_content_vector(),
    )]
    content: Vec<String>,
    #[values("", rand_string(rand_usize(1, 10)))] name: String,
) {
    let id = rand_usize(0, 42);
    let connectors = random_connectors();
    let weight = rand_float();

    let tile = Tile {
        id,
        name: name.clone(),
        content: TileContent::Text(content.clone()),
        connectors: Connectors { ..connectors },
        weight,
    };

    assert_eq!(tile.id, id);
    assert_eq!(tile.name, name);
    if let TileContent::Text(actual_content) = tile.content {
        assert_eq!(actual_content, content);
    } else {
        panic!("Expected the Text variant, got something else")
    }
    assert_eq!(tile.connectors, connectors);
    assert_eq!(tile.weight, weight);
}

// noinspection DuplicatedCode
#[rstest]
// region valid content, zero lines
#[case::valid_content_zero_lines(
    rand_string(rand_usize(1, 10)),
    vec![],
    random_connectors(),
    rand_float(),
    None
)]
// endregion
// region valid content, one line
#[case::valid_content_one_line(
    rand_string(rand_usize(1, 10)),
    vec![rand_string(3)],
    random_connectors(),
    rand_float(),
    None
)]
// endregion
// region valid content, multiple lines
#[case::valid_content_multiple_lines(
    rand_string(rand_usize(1, 10)),
    random_text_tile_content_vector(),
    random_connectors(),
    rand_float(),
    None
)]
// endregion
// region runtime error: too short third line
#[case::runtime_error_too_short_third_line(
    RANDOM_STRINGS[5].clone(),
    vec![
        RANDOM_STRINGS[4].clone(),
        RANDOM_STRINGS[4].clone(),
        RANDOM_STRINGS[3].clone(),
        RANDOM_STRINGS[4].clone(),
        RANDOM_STRINGS[4].clone(),
        RANDOM_STRINGS[4].clone(),
    ],
    random_connectors(),
    rand_float(),
    Some(Error::ValueError(format!(
        "line lengths of the tile with the name “{}” differ among each other:\n{:#?}",
        RANDOM_STRINGS[5],
        vec![
            &RANDOM_STRINGS[4],
            &RANDOM_STRINGS[4],
            &RANDOM_STRINGS[3],
            &RANDOM_STRINGS[4],
            &RANDOM_STRINGS[4],
            &RANDOM_STRINGS[4],
        ]
    )))
)]
// endregion
// region runtime error: too long first line
#[case::runtime_error_too_long_first_line(
    RANDOM_STRINGS[4].clone(),
    vec![
        RANDOM_STRINGS[6].clone(),
        RANDOM_STRINGS[4].clone(),
        RANDOM_STRINGS[4].clone(),
        RANDOM_STRINGS[4].clone(),
    ],
    random_connectors(),
    rand_float(),
    Some(Error::ValueError(format!(
        "line lengths of the tile with the name “{}” differ among each other:\n{:#?}",
        RANDOM_STRINGS[4],
        vec![
            &RANDOM_STRINGS[6],
            &RANDOM_STRINGS[4],
            &RANDOM_STRINGS[4],
            &RANDOM_STRINGS[4],
        ]
    )))
)]
// endregion
// region runtime error too short last line
#[case::runtime_error_too_short_last_line(
    RANDOM_STRINGS[3].clone(),
    vec![
        RANDOM_STRINGS[4].clone(),
        RANDOM_STRINGS[4].clone(),
        RANDOM_STRINGS[0].clone(),
    ],
    random_connectors(),
    rand_float(),
    Some(Error::ValueError(format!(
        "line lengths of the tile with the name “{}” differ among each other:\n{:#?}",
        RANDOM_STRINGS[3],
        vec![
            &RANDOM_STRINGS[4],
            &RANDOM_STRINGS[4],
            &RANDOM_STRINGS[0],
        ]
    )))
)]
// endregion
fn test_tile_new_text(
    #[case] name: String,
    #[case] content: Vec<String>,
    #[case] connectors: Connectors,
    #[case] weight: f64,
    #[case] expected_error: Option<Error>,
) {
    let id = rand_usize(4, 23);
    let result = Tile::new_text(
        id,
        name.clone(),
        content.clone(),
        Connectors { ..connectors },
        weight,
    );

    if let Some(expected_error) = expected_error {
        assert_expected_error(result, expected_error);
    } else {
        let tile = result.expect("line lengths should match");

        assert_eq!(tile.id, id);
        assert_eq!(tile.name, name);
        if let TileContent::Text(actual_content) = tile.content {
            assert_eq!(actual_content, content);
        } else {
            panic!("Expected the Text variant, got something else")
        }
        assert_eq!(tile.connectors, connectors);
        assert_eq!(tile.weight, weight);
    }
}

// noinspection DuplicatedCode
#[rstest]
#[case::existing_valid_file_using_string_one_row_zero_cols(
    rand_string(rand_usize(2, 27)),
    test_data("text_tile_one_row_zero_cols"),
    vec!["".to_string()],
    random_connectors(),
    rand_float(),
    None
)]
#[case::existing_valid_file_using_path_buf_one_row_one_col(
    rand_string(rand_usize(2, 27)),
    PathBuf::from(test_data("text_tile_one_row_one_col")),
    vec!["ü".to_string()],
    random_connectors(),
    rand_float(),
    None
)]
#[case::existing_valid_file_using_path_buf_two_rows_zero_cols(
    rand_string(rand_usize(2, 27)),
    PathBuf::from(test_data("text_tile_two_rows_zero_cols")),
    vec!["".to_string(), "".to_string()],
    random_connectors(),
    rand_float(),
    None
)]
#[case::existing_valid_file_using_path_buf_two_rows_three_cols(
    rand_string(rand_usize(2, 27)),
    PathBuf::from(test_data("text_tile_two_rows_three_cols")),
    vec!["ita".to_string(), "lew".to_string()],
    random_connectors(),
    rand_float(),
    None
)]
#[case::existing_invalid_file_using_path_buf_two_rows_three_cols(
    RANDOM_STRINGS[3].clone(),
    PathBuf::from(test_data("text_tile_two_rows_three_cols_invalid")),
    vec![],
    random_connectors(),
    rand_float(),
    Some(Error::ValueError(format!(
        "\
line lengths of the tile with the name “{name}” differ among each other:
[
    \"ita\",
    \"lw\",
]",
        name = RANDOM_STRINGS[3]
    )))
)]
#[case::nonexistent_file_using_path(
    rand_string(rand_usize(2, 27)),
    Path::new("invalid/path"),
    vec![],
    random_connectors(),
    rand_float(),
    Some(Error::IoError(
        "the tile file located at \"invalid/path\" could not be opened: No such file or directory \
        (os error 2)"
            .to_string(),
    ))
)]
#[case::nonexistent_file_using_string_slice(
    rand_string(rand_usize(2, 27)),
    "text_tile_empty",
    vec![],
    random_connectors(),
    rand_float(),
    Some(Error::IoError(
        "the tile file located at \"text_tile_empty\" could not be opened: No such file or \
        directory (os error 2)"
            .to_string()
    ))
)]
fn test_tile_new_text_from_file<P: AsRef<Path> + Debug + Clone>(
    #[case] name: String,
    #[case] file_path: P,
    #[case] expected_content: Vec<String>,
    #[case] connectors: Connectors,
    #[case] weight: f64,
    #[case] expected_error: Option<Error>,
) {
    let id = rand_usize(24, 29);
    let result = Tile::new_text_from_file(
        id,
        name.clone(),
        file_path,
        Connectors { ..connectors },
        weight,
    );

    if let Some(expected_error) = expected_error {
        match result {
            Ok(_) => panic!("Expected an Err, got Ok"),
            Err(error) => assert_eq!(error, expected_error),
        }
    } else {
        let tile = result.expect("line lengths should match");

        assert_eq!(tile.id, id);
        assert_eq!(tile.name, name);
        if let TileContent::Text(actual_content) = tile.content {
            assert_eq!(actual_content, expected_content);
        } else {
            panic!("Expected the Text variant, got something else")
        }
        assert_eq!(tile.connectors, connectors);
        assert_eq!(tile.weight, weight);
    }
}

#[rstest]
fn test_tile_raster_image_raw_initialization(
    #[values(
        DynamicImage::ImageRgba8(RgbaImage::new(0, 0)),
        image::open(test_data("four_raster_image_tiles/pixel/blue_pixel.png"))
            .expect("blue_pixel.png should be accessible"),
        image::open(test_data("four_raster_image_tiles/pixel/tile-small.jpg"))
            .expect("tile-small.jpg should be accessible"),
        image::open(test_data("four_raster_image_tiles/some/other/red-white.dds"))
            .expect("red-white.dds should be accessible"),
    )]
    content: DynamicImage,
    #[values("", rand_string(rand_usize(1, 10)))] name: String,
) {
    let id = rand_usize(0, 43);
    let connectors = random_connectors();
    let weight = rand_float();

    let tile = Tile {
        id,
        name: name.clone(),
        content: TileContent::Raster(content.clone()),
        connectors: Connectors { ..connectors },
        weight,
    };

    assert_eq!(tile.id, id);
    assert_eq!(tile.name, name);
    if let TileContent::Raster(actual_content) = tile.content {
        assert_eq!(actual_content, content);
    } else {
        panic!("Expected the Raster variant, got something else")
    }
    assert_eq!(tile.connectors, connectors);
    assert_eq!(tile.weight, weight);
}

#[rstest]
fn test_tile_new_raster_image(
    #[values(
        DynamicImage::ImageRgba8(RgbaImage::new(17, 6)),
        image::open(test_data("four_raster_image_tiles/pixel/blue_pixel.png"))
            .expect("blue_pixel.png should be accessible"),
        image::open(test_data("four_raster_image_tiles/pixel/tile-small.jpg"))
            .expect("tile-small.jpg should be accessible"),
        image::open(test_data("four_raster_image_tiles/some/other/red-white.dds"))
            .expect("red-white.dds should be accessible"),
    )]
    content: DynamicImage,
    #[values("", rand_string(rand_usize(8, 27)))] name: String,
) {
    let id = rand_usize(100, 142);
    let connectors = random_connectors();
    let weight = rand_float();

    let tile = Tile::new_raster_image(
        id,
        name.clone(),
        content.clone(),
        Connectors { ..connectors },
        weight,
    );

    assert_eq!(tile.id, id);
    assert_eq!(tile.name, name);
    if let TileContent::Raster(actual_content) = tile.content {
        assert_eq!(actual_content, content);
    } else {
        panic!("Expected the Raster variant, got something else")
    }
    assert_eq!(tile.connectors, connectors);
    assert_eq!(tile.weight, weight);
}

#[rstest]
#[case::existing_valid_jpg_file_using_string(
    rand_usize(0, 100),
    rand_string(rand_usize(7, 17)),
    test_data("four_raster_image_tiles/pixel/tile-small.jpg"),
    image::open(test_data("four_raster_image_tiles/pixel/tile-small.jpg"))
        .expect("tile-small.jpg should be accessible"),
    random_connectors(),
    rand_float(),
    None
)]
#[case::existing_valid_png_file_using_path_buf(
    rand_usize(0, 100),
    rand_string(rand_usize(7, 17)),
    PathBuf::from(test_data("four_raster_image_tiles/pixel/blue_pixel.png")),
    image::open(test_data("four_raster_image_tiles/pixel/blue_pixel.png"))
        .expect("blue_pixel.png should be accessible"),
    random_connectors(),
    rand_float(),
    None
)]
#[case::existing_invalid_jpg_file_using_path(
    rand_usize(0, 100),
    rand_string(rand_usize(7, 17)),
    PathBuf::from(test_data("tile-arithmetic.jpg")),
    DynamicImage::ImageRgb8(ImageBuffer::new(0, 0)),
    random_connectors(),
    rand_float(),
    Some(Error::IoError(format!(
        "the tile file located at \"{}\" could not be loaded: Format error decoding Jpeg: \
        \"Parsing of the following header `DAC` is not supported,cannot continue\"",
        test_data("tile-arithmetic.jpg"),
    )))
)]
#[case::nonexistent_file_using_string_slice(
    rand_usize(0, 100),
    rand_string(rand_usize(7, 17)),
    "tile-arithmetic.jpg",
    DynamicImage::ImageRgb8(ImageBuffer::new(0, 0)),
    random_connectors(),
    rand_float(),
    Some(Error::IoError(
        "the tile file located at \"tile-arithmetic.jpg\" could not be loaded: \
        No such file or directory (os error 2)"
            .to_string()
    ))
)]
fn test_tile_new_raster_image_from_file<P: AsRef<Path> + Debug + Clone>(
    #[case] id: usize,
    #[case] name: String,
    #[case] file_path: P,
    #[case] expected_content: DynamicImage,
    #[case] connectors: Connectors,
    #[case] weight: f64,
    #[case] expected_error: Option<Error>,
) {
    let result = Tile::new_raster_image_from_file(
        id,
        name.clone(),
        file_path,
        Connectors { ..connectors },
        weight,
    );

    if let Some(expected_error) = expected_error {
        let error = result.expect_err("Expected an Err, got Ok");
        assert_eq!(expected_error, error);
    } else {
        let tile = result.expect("the image file should be accessible");

        assert_eq!(tile.id, id);
        assert_eq!(tile.name, name);
        if let TileContent::Raster(actual_content) = tile.content {
            assert_eq!(expected_content, actual_content);
        } else {
            panic!("Expected the Raster variant, got something else")
        }
        assert_eq!(connectors, tile.connectors);
        assert_eq!(weight, tile.weight);
    }
}

#[rstest]
#[case::valid_raster(TileType::Raster)]
#[case::valid_text(TileType::Text)]
#[should_panic(expected = "not implemented")]
#[case::invalid_raster_file_tile_arithmetic_jpg(TileType::Svg)]
fn test_tile_determine_constructor_by_tile_type(#[case] tile_type: TileType) {
    let result = Tile::determine_constructor_by_tile_type(&tile_type);

    assert_eq!(
        // The function pointers are not used in the case parametrizations as usual because
        // working out the necessary type declaration for expected_function proved unfeasible.
        match tile_type {
            TileType::Raster => Tile::new_raster_image_from_file::<PathBuf>,
            TileType::Svg => {
                panic!("SVG support is not implemented yet");
            },
            TileType::Text => Tile::new_text_from_file,
        },
        result
    );
}
